Merge "Change name of main page in Sardinian (sc)"
[lhc/web/wiklou.git] / resources / lib / ooui / oojs-ui-core.js
index 3b76a0c..b2b7bfe 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOUI v0.30.4
+ * OOUI v0.31.2
  * https://www.mediawiki.org/wiki/OOUI
  *
  * Copyright 2011–2019 OOUI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2019-03-07T09:14:18Z
+ * Date: 2019-03-26T23:00:40Z
  */
 ( function ( OO ) {
 
@@ -295,7 +295,7 @@ OO.ui.throttle = function ( func, wait ) {
                previous = 0,
                run = function () {
                        timeout = null;
-                       previous = OO.ui.now();
+                       previous = Date.now();
                        func.apply( context, args );
                };
        return function () {
@@ -304,7 +304,7 @@ OO.ui.throttle = function ( func, wait ) {
                // period. If it's less, run the function immediately. If it's more,
                // set a timeout for the remaining time -- but don't replace an
                // existing timeout, since that'd indefinitely prolong the wait.
-               var remaining = wait - ( OO.ui.now() - previous );
+               var remaining = wait - ( Date.now() - previous );
                context = this;
                args = arguments;
                if ( remaining <= 0 ) {
@@ -323,10 +323,12 @@ OO.ui.throttle = function ( func, wait ) {
 /**
  * A (possibly faster) way to get the current timestamp as an integer.
  *
+ * @deprecated Since 0.31.1; use `Date.now()` instead.
  * @return {number} Current timestamp, in milliseconds since the Unix epoch
  */
-OO.ui.now = Date.now || function () {
-       return new Date().getTime();
+OO.ui.now = function () {
+       OO.ui.warnDeprecation( 'OO.ui.now() is deprecated, use Date.now() instead' );
+       return Date.now();
 };
 
 /**
@@ -659,7 +661,8 @@ OO.ui.Element = function OoUiElement( config ) {
                this.$element.append( config.content.map( function ( v ) {
                        if ( typeof v === 'string' ) {
                                // Escape string so it is properly represented in HTML.
-                               return document.createTextNode( v );
+                               // Don't create empty text nodes for empty strings.
+                               return v ? document.createTextNode( v ) : undefined;
                        } else if ( v instanceof OO.ui.HtmlSnippet ) {
                                // Bypass escaping.
                                return v.toString();
@@ -2117,7 +2120,7 @@ OO.ui.mixin.TabIndexedElement.prototype.getInputId = function () {
 OO.ui.mixin.TabIndexedElement.prototype.isLabelableNode = function ( $node ) {
        var
                labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
-               tagName = $node.prop( 'tagName' ).toLowerCase();
+               tagName = ( $node.prop( 'tagName' ) || '' ).toLowerCase();
 
        if ( tagName === 'input' && $node.attr( 'type' ) !== 'hidden' ) {
                return true;
@@ -2301,12 +2304,6 @@ OO.ui.mixin.ButtonElement.prototype.onDocumentMouseUp = function ( e ) {
        this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseUpHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.mixin.ButtonElement.prototype.onMouseUp = function () {
-       OO.ui.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
-       this.onDocumentMouseUp.apply( this, arguments );
-};
-
 /**
  * Handles mouse click events.
  *
@@ -2354,12 +2351,6 @@ OO.ui.mixin.ButtonElement.prototype.onDocumentKeyUp = function ( e ) {
        this.getElementDocument().removeEventListener( 'keyup', this.onDocumentKeyUpHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.mixin.ButtonElement.prototype.onKeyUp = function () {
-       OO.ui.warnDeprecation( 'onKeyUp is deprecated, use onDocumentKeyUp instead' );
-       this.onDocumentKeyUp.apply( this, arguments );
-};
-
 /**
  * Handles key press events.
  *
@@ -2928,8 +2919,6 @@ OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
  *  See the [OOUI documentation on MediaWiki] [2] for a list of icons included in the library.
  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
- * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that
- *  returns title text. The icon title is displayed when users move the mouse over the icon.
  */
 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
        // Configuration initialization
@@ -2938,16 +2927,9 @@ OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
        // Properties
        this.$icon = null;
        this.icon = null;
-       this.iconTitle = null;
-
-       // `iconTitle`s are deprecated since 0.30.0
-       if ( config.iconTitle !== undefined ) {
-               OO.ui.warnDeprecation( 'IconElement: Widgets with iconTitle set are deprecated, use title instead. See T76638 for details.' );
-       }
 
        // Initialization
        this.setIcon( config.icon || this.constructor.static.icon );
-       this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
        this.setIconElement( config.$icon || $( '<span>' ) );
 };
 
@@ -3049,40 +3031,6 @@ OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
        return this;
 };
 
-/**
- * Set the icon title. Use `null` to remove the title.
- *
- * @param {string|Function|null} iconTitle A text string used as the icon title,
- *  a function that returns title text, or `null` for no title.
- * @chainable
- * @return {OO.ui.Element} The element, for chaining
- * @deprecated
- */
-OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
-       iconTitle =
-               ( typeof iconTitle === 'function' || ( typeof iconTitle === 'string' && iconTitle.length ) ) ?
-                       OO.ui.resolveMsg( iconTitle ) : null;
-
-       if ( this.iconTitle !== iconTitle ) {
-               this.iconTitle = iconTitle;
-               if ( this.$icon ) {
-                       if ( this.iconTitle !== null ) {
-                               this.$icon.attr( 'title', iconTitle );
-                       } else {
-                               this.$icon.removeAttr( 'title' );
-                       }
-               }
-       }
-
-       // `setIconTitle` is deprecated since 0.30.0
-       if ( iconTitle !== null ) {
-               // Avoid a warning when this is called from the constructor with no iconTitle set
-               OO.ui.warnDeprecation( 'IconElement: setIconTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
-       }
-
-       return this;
-};
-
 /**
  * Get the symbolic name of the icon.
  *
@@ -3127,9 +3075,6 @@ OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
  *  See the [OOUI documentation on MediaWiki][2] for a list of indicators included
  *  in the library.
  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
- * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
- *  or a function that returns title text. The indicator title is displayed when users move
- *  the mouse over the indicator.
  */
 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
        // Configuration initialization
@@ -3138,16 +3083,9 @@ OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
        // Properties
        this.$indicator = null;
        this.indicator = null;
-       this.indicatorTitle = null;
-
-       // `indicatorTitle`s are deprecated since 0.30.0
-       if ( config.indicatorTitle !== undefined ) {
-               OO.ui.warnDeprecation( 'IndicatorElement: Widgets with indicatorTitle set are deprecated, use title instead. See T76638 for details.' );
-       }
 
        // Initialization
        this.setIndicator( config.indicator || this.constructor.static.indicator );
-       this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
        this.setIndicatorElement( config.$indicator || $( '<span>' ) );
 };
 
@@ -3237,42 +3175,6 @@ OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
        return this;
 };
 
-/**
- * Set the indicator title.
- *
- * The title is displayed when a user moves the mouse over the indicator.
- *
- * @param {string|Function|null} indicatorTitle Indicator title text, a function that returns text,
- *  or `null` for no indicator title
- * @chainable
- * @return {OO.ui.Element} The element, for chaining
- * @deprecated
- */
-OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
-       indicatorTitle =
-               ( typeof indicatorTitle === 'function' || ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ) ?
-                       OO.ui.resolveMsg( indicatorTitle ) : null;
-
-       if ( this.indicatorTitle !== indicatorTitle ) {
-               this.indicatorTitle = indicatorTitle;
-               if ( this.$indicator ) {
-                       if ( this.indicatorTitle !== null ) {
-                               this.$indicator.attr( 'title', indicatorTitle );
-                       } else {
-                               this.$indicator.removeAttr( 'title' );
-                       }
-               }
-       }
-
-       // `setIndicatorTitle` is deprecated since 0.30.0
-       if ( indicatorTitle !== null ) {
-               // Avoid a warning when this is called from the constructor with no indicatorTitle set
-               OO.ui.warnDeprecation( 'IndicatorElement: setIndicatorTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
-       }
-
-       return this;
-};
-
 /**
  * Get the symbolic name of the indicator (e.g., ‘clear’ or  ‘down’).
  *
@@ -3836,16 +3738,16 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        OO.ui.mixin.IconElement.call( this, config );
        OO.ui.mixin.IndicatorElement.call( this, config );
        OO.ui.mixin.LabelElement.call( this, config );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.TitledElement.call( this, $.extend( {
                $titled: this.$button
-       } ) );
+       }, config ) );
        OO.ui.mixin.FlaggedElement.call( this, config );
-       OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.TabIndexedElement.call( this, $.extend( {
                $tabIndexed: this.$button
-       } ) );
-       OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {
                $accessKeyed: this.$button
-       } ) );
+       }, config ) );
 
        // Properties
        this.href = null;
@@ -4065,9 +3967,9 @@ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
        OO.ui.ButtonGroupWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.GroupElement.call( this, $.extend( {
                $group: this.$element
-       } ) );
+       }, config ) );
        OO.ui.mixin.TitledElement.call( this, config );
 
        // Initialization
@@ -4153,19 +4055,19 @@ OO.ui.IconWidget = function OoUiIconWidget( config ) {
        OO.ui.IconWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.IconElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.IconElement.call( this, $.extend( {
                $icon: this.$element
-       } ) );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.TitledElement.call( this, $.extend( {
                $titled: this.$element
-       } ) );
-       OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.LabelElement.call( this, $.extend( {
                $label: this.$element,
                invisibleLabel: true
-       } ) );
-       OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.FlaggedElement.call( this, $.extend( {
                $flagged: this.$element
-       } ) );
+       }, config ) );
 
        // Initialization
        this.$element.addClass( 'oo-ui-iconWidget' );
@@ -4228,16 +4130,16 @@ OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
        OO.ui.IndicatorWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.IndicatorElement.call( this, $.extend( {
                $indicator: this.$element
-       } ) );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.TitledElement.call( this, $.extend( {
                $titled: this.$element
-       } ) );
-       OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.LabelElement.call( this, $.extend( {
                $label: this.$element,
                invisibleLabel: true
-       } ) );
+       }, config ) );
 
        // Initialization
        this.$element.addClass( 'oo-ui-indicatorWidget' );
@@ -4309,9 +4211,9 @@ OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
        OO.ui.LabelWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.LabelElement.call( this, $.extend( {
                $label: this.$element
-       } ) );
+       }, config ) );
        OO.ui.mixin.TitledElement.call( this, config );
 
        // Properties
@@ -5396,10 +5298,10 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
 
        // Mixin constructors
        OO.ui.mixin.LabelElement.call( this, config );
-       OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.ClippableElement.call( this, $.extend( {
                $clippable: this.$body,
                $clippableContainer: this.$popup
-       } ) );
+       }, config ) );
        OO.ui.mixin.FloatableElement.call( this, config );
 
        // Properties
@@ -5498,12 +5400,6 @@ OO.ui.PopupWidget.prototype.onDocumentMouseDown = function ( e ) {
        }
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.PopupWidget.prototype.onMouseDown = function () {
-       OO.ui.warnDeprecation( 'onMouseDown is deprecated, use onDocumentMouseDown instead' );
-       this.onDocumentMouseDown.apply( this, arguments );
-};
-
 /**
  * Bind document mouse down listener.
  *
@@ -5521,12 +5417,6 @@ OO.ui.PopupWidget.prototype.bindDocumentMouseDownListener = function () {
        this.getElementDocument().addEventListener( 'click', this.onDocumentMouseDownHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
-       OO.ui.warnDeprecation( 'bindMouseDownListener is deprecated, use bindDocumentMouseDownListener instead' );
-       this.bindDocumentMouseDownListener.apply( this, arguments );
-};
-
 /**
  * Handles close button click events.
  *
@@ -5548,12 +5438,6 @@ OO.ui.PopupWidget.prototype.unbindDocumentMouseDownListener = function () {
        this.getElementDocument().removeEventListener( 'click', this.onDocumentMouseDownHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
-       OO.ui.warnDeprecation( 'unbindMouseDownListener is deprecated, use unbindDocumentMouseDownListener instead' );
-       this.unbindDocumentMouseDownListener.apply( this, arguments );
-};
-
 /**
  * Handles document key down events.
  *
@@ -5580,12 +5464,6 @@ OO.ui.PopupWidget.prototype.bindDocumentKeyDownListener = function () {
        this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
-       OO.ui.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
-       this.bindDocumentKeyDownListener.apply( this, arguments );
-};
-
 /**
  * Unbind document key down listener.
  *
@@ -5595,12 +5473,6 @@ OO.ui.PopupWidget.prototype.unbindDocumentKeyDownListener = function () {
        this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
-       OO.ui.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
-       this.unbindDocumentKeyDownListener.apply( this, arguments );
-};
-
 /**
  * Show, hide, or toggle the visibility of the anchor.
  *
@@ -6311,9 +6183,9 @@ OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
        OO.ui.mixin.TitledElement.call( this, config );
 
        // Properties
-       this.selected = false;
        this.highlighted = false;
        this.pressed = false;
+       this.setSelected( !!config.selected );
 
        // Initialization
        this.$element
@@ -6321,7 +6193,6 @@ OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
                // Allow programmatic focussing (and by access key), but not tabbing
                .attr( 'tabindex', '-1' )
                .attr( 'role', 'option' )
-               .attr( 'aria-selected', 'false' )
                .addClass( 'oo-ui-optionWidget' )
                .append( this.$label );
 };
@@ -6551,6 +6422,7 @@ OO.ui.OptionWidget.prototype.getMatchText = function () {
  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
  *  the [OOUI documentation on MediaWiki] [2] for examples.
  *  [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
+ * @cfg {boolean} [multiselect] Allow for multiple selections
  */
 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        // Configuration initialization
@@ -6560,13 +6432,14 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        OO.ui.SelectWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, {
+       OO.ui.mixin.GroupWidget.call( this, $.extend( {
                $group: this.$element
-       } ) );
+       }, config ) );
 
        // Properties
        this.pressed = false;
        this.selecting = null;
+       this.multiselect = !!config.multiselect;
        this.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this );
        this.onDocumentMouseMoveHandler = this.onDocumentMouseMove.bind( this );
        this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
@@ -6627,13 +6500,16 @@ OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
  * A `select` event is emitted when the selection is modified programmatically with the #selectItem
  * method.
  *
- * @param {OO.ui.OptionWidget|null} item Selected item
+ * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
  */
 
 /**
  * @event choose
+ *
  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
+ *
  * @param {OO.ui.OptionWidget} item Chosen item
+ * @param {boolean} selected Item is selected
  */
 
 /**
@@ -6770,12 +6646,6 @@ OO.ui.SelectWidget.prototype.onDocumentMouseUp = function ( e ) {
        return false;
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.onMouseUp = function () {
-       OO.ui.warnDeprecation( 'onMouseUp is deprecated, use onDocumentMouseUp instead' );
-       this.onDocumentMouseUp.apply( this, arguments );
-};
-
 /**
  * Handle document mouse move events.
  *
@@ -6794,12 +6664,6 @@ OO.ui.SelectWidget.prototype.onDocumentMouseMove = function ( e ) {
        }
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.onMouseMove = function () {
-       OO.ui.warnDeprecation( 'onMouseMove is deprecated, use onDocumentMouseMove instead' );
-       this.onDocumentMouseMove.apply( this, arguments );
-};
-
 /**
  * Handle mouse over events.
  *
@@ -6842,12 +6706,13 @@ OO.ui.SelectWidget.prototype.onMouseLeave = function () {
 OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) {
        var nextItem,
                handled = false,
-               currentItem = this.findHighlightedItem() || this.findSelectedItem();
+               currentItem = this.findHighlightedItem(),
+               firstItem = this.getItems()[ 0 ];
 
        if ( !this.isDisabled() && this.isVisible() ) {
                switch ( e.keyCode ) {
                        case OO.ui.Keys.ENTER:
-                               if ( currentItem && currentItem.constructor.static.highlightable ) {
+                               if ( currentItem ) {
                                        // Was only highlighted, now let's select it. No-op if already selected.
                                        this.chooseItem( currentItem );
                                        handled = true;
@@ -6856,18 +6721,18 @@ OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) {
                        case OO.ui.Keys.UP:
                        case OO.ui.Keys.LEFT:
                                this.clearKeyPressBuffer();
-                               nextItem = this.findRelativeSelectableItem( currentItem, -1 );
+                               nextItem = currentItem ? this.findRelativeSelectableItem( currentItem, -1 ) : firstItem;
                                handled = true;
                                break;
                        case OO.ui.Keys.DOWN:
                        case OO.ui.Keys.RIGHT:
                                this.clearKeyPressBuffer();
-                               nextItem = this.findRelativeSelectableItem( currentItem, 1 );
+                               nextItem = currentItem ? this.findRelativeSelectableItem( currentItem, 1 ) : firstItem;
                                handled = true;
                                break;
                        case OO.ui.Keys.ESCAPE:
                        case OO.ui.Keys.TAB:
-                               if ( currentItem && currentItem.constructor.static.highlightable ) {
+                               if ( currentItem ) {
                                        currentItem.setHighlighted( false );
                                }
                                this.unbindDocumentKeyDownListener();
@@ -6893,12 +6758,6 @@ OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) {
        }
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.onKeyDown = function () {
-       OO.ui.warnDeprecation( 'onKeyDown is deprecated, use onDocumentKeyDown instead' );
-       this.onDocumentKeyDown.apply( this, arguments );
-};
-
 /**
  * Bind document key down listener.
  *
@@ -6908,12 +6767,6 @@ OO.ui.SelectWidget.prototype.bindDocumentKeyDownListener = function () {
        this.getElementDocument().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
-       OO.ui.warnDeprecation( 'bindKeyDownListener is deprecated, use bindDocumentKeyDownListener instead' );
-       this.bindDocumentKeyDownListener.apply( this, arguments );
-};
-
 /**
  * Unbind document key down listener.
  *
@@ -6923,12 +6776,6 @@ OO.ui.SelectWidget.prototype.unbindDocumentKeyDownListener = function () {
        this.getElementDocument().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
-       OO.ui.warnDeprecation( 'unbindKeyDownListener is deprecated, use unbindDocumentKeyDownListener instead' );
-       this.unbindDocumentKeyDownListener.apply( this, arguments );
-};
-
 /**
  * Scroll item into view, preventing spurious mouse highlight actions from happening.
  *
@@ -7018,12 +6865,6 @@ OO.ui.SelectWidget.prototype.onDocumentKeyPress = function ( e ) {
        e.stopPropagation();
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.onKeyPress = function () {
-       OO.ui.warnDeprecation( 'onKeyPress is deprecated, use onDocumentKeyPress instead' );
-       this.onDocumentKeyPress.apply( this, arguments );
-};
-
 /**
  * Get a matcher for the specific string
  *
@@ -7071,12 +6912,6 @@ OO.ui.SelectWidget.prototype.bindDocumentKeyPressListener = function () {
        this.getElementDocument().addEventListener( 'keypress', this.onDocumentKeyPressHandler, true );
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
-       OO.ui.warnDeprecation( 'bindKeyPressListener is deprecated, use bindDocumentKeyPressListener instead' );
-       this.bindDocumentKeyPressListener.apply( this, arguments );
-};
-
 /**
  * Unbind document key down listener.
  *
@@ -7090,12 +6925,6 @@ OO.ui.SelectWidget.prototype.unbindDocumentKeyPressListener = function () {
        this.clearKeyPressBuffer();
 };
 
-// Deprecated alias since 0.28.3
-OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
-       OO.ui.warnDeprecation( 'unbindKeyPressListener is deprecated, use unbindDocumentKeyPressListener instead' );
-       this.unbindDocumentKeyPressListener.apply( this, arguments );
-};
-
 /**
  * Visibility change handler
  *
@@ -7123,20 +6952,36 @@ OO.ui.SelectWidget.prototype.findTargetItem = function ( e ) {
        return $option.data( 'oo-ui-optionWidget' ) || null;
 };
 
+/**
+ * Find all selected items, if there are any. If the widget allows for multiselect
+ * it will return an array of selected options. If the widget doesn't allow for
+ * multiselect, it will return the selected option or null if no item is selected.
+ *
+ * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
+ *  then return an array of selected items (or empty array),
+ *  if the widget is not multiselect, return a single selected item, or `null`
+ *  if no item is selected
+ */
+OO.ui.SelectWidget.prototype.findSelectedItems = function () {
+       var selected = this.items.filter( function ( item ) {
+               return item.isSelected();
+       } );
+
+       return this.multiselect ?
+               selected :
+               selected[ 0 ] || null;
+};
+
 /**
  * Find selected item.
  *
- * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
+ * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
+ *  then return an array of selected items (or empty array),
+ *  if the widget is not multiselect, return a single selected item, or `null`
+ *  if no item is selected
  */
 OO.ui.SelectWidget.prototype.findSelectedItem = function () {
-       var i, len;
-
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               if ( this.items[ i ].isSelected() ) {
-                       return this.items[ i ];
-               }
-       }
-       return null;
+       return this.findSelectedItems();
 };
 
 /**
@@ -7283,6 +7128,30 @@ OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
        return this.selectItem( itemFromData );
 };
 
+/**
+ * Programmatically unselect an option by its reference. If the widget
+ * allows for multiple selections, there may be other items still selected;
+ * otherwise, no items will be selected.
+ * If no item is given, all selected items will be unselected.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to unselect
+ * @fires select
+ * @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
+ */
+OO.ui.SelectWidget.prototype.unselectItem = function ( item ) {
+       if ( item ) {
+               item.setSelected( false );
+       } else {
+               this.items.forEach( function ( item ) {
+                       item.setSelected( false );
+               } );
+       }
+
+       this.emit( 'select', this.findSelectedItems() );
+       return this;
+};
+
 /**
  * Programmatically select an option by its reference. If the `item` parameter is omitted,
  * all options will be deselected.
@@ -7296,14 +7165,20 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
        var i, len, selected,
                changed = false;
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               selected = this.items[ i ] === item;
-               if ( this.items[ i ].isSelected() !== selected ) {
-                       this.items[ i ].setSelected( selected );
-                       changed = true;
+       if ( this.multiselect && item ) {
+               // Select the item directly
+               item.setSelected( true );
+       } else {
+               for ( i = 0, len = this.items.length; i < len; i++ ) {
+                       selected = this.items[ i ] === item;
+                       if ( this.items[ i ].isSelected() !== selected ) {
+                               this.items[ i ].setSelected( selected );
+                               changed = true;
+                       }
                }
        }
        if ( changed ) {
+               // TODO: When should a non-highlightable element be selected?
                if ( item && !item.constructor.static.highlightable ) {
                        if ( item ) {
                                this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() );
@@ -7311,7 +7186,7 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
                                this.$focusOwner.removeAttr( 'aria-activedescendant' );
                        }
                }
-               this.emit( 'select', item );
+               this.emit( 'select', this.findSelectedItems() );
        }
 
        return this;
@@ -7364,8 +7239,13 @@ OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
  */
 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
        if ( item ) {
-               this.selectItem( item );
-               this.emit( 'choose', item );
+               if ( this.multiselect && item.isSelected() ) {
+                       this.unselectItem( item );
+               } else {
+                       this.selectItem( item );
+               }
+
+               this.emit( 'choose', item, item.isSelected() );
        }
 
        return this;
@@ -7649,6 +7529,7 @@ OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
        this.$element
                .addClass( 'oo-ui-menuSectionOptionWidget' )
                .removeAttr( 'role aria-selected' );
+       this.selected = false;
 };
 
 /* Setup */
@@ -7730,7 +7611,7 @@ OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
        OO.ui.MenuSelectWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
+       OO.ui.mixin.ClippableElement.call( this, $.extend( { $clippable: this.$group }, config ) );
        OO.ui.mixin.FloatableElement.call( this, config );
 
        // Initial vertical positions other than 'center' will result in
@@ -7833,7 +7714,7 @@ OO.ui.MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
                                break;
                        case OO.ui.Keys.ESCAPE:
                        case OO.ui.Keys.TAB:
-                               if ( currentItem ) {
+                               if ( currentItem && !this.multiselect ) {
                                        currentItem.setHighlighted( false );
                                }
                                this.toggle( false );
@@ -7890,10 +7771,6 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
                        section.toggle( showAll || !sectionEmpty );
                }
 
-               if ( anyVisible && this.items.length && !exactMatch ) {
-                       this.scrollItemIntoView( this.items[ 0 ] );
-               }
-
                if ( !anyVisible ) {
                        this.highlightItem( null );
                }
@@ -8050,7 +7927,7 @@ OO.ui.MenuSelectWidget.prototype.clearItems = function () {
  * @inheritdoc
  */
 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
-       var change, originalHeight, flippedHeight;
+       var change, originalHeight, flippedHeight, selectedItem;
 
        visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
        change = visible !== this.isVisible();
@@ -8115,9 +7992,13 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 
                        this.$focusOwner.attr( 'aria-expanded', 'true' );
 
-                       if ( this.findSelectedItem() ) {
-                               this.$focusOwner.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
-                               this.findSelectedItem().scrollElementIntoView( { duration: 0 } );
+                       selectedItem = this.findSelectedItem();
+                       if ( !this.multiselect && selectedItem ) {
+                               // TODO: Verify if this is even needed; This is already done on highlight changes
+                               // in SelectWidget#highlightItem, so we should just need to highlight the item we need to
+                               // highlight here and not bother with attr or checking selections.
+                               this.$focusOwner.attr( 'aria-activedescendant', selectedItem.getElementId() );
+                               selectedItem.scrollElementIntoView( { duration: 0 } );
                        }
 
                        // Auto-hide
@@ -8140,6 +8021,13 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
        return this;
 };
 
+/**
+ * Scroll to the top of the menu
+ */
+OO.ui.MenuSelectWidget.prototype.scrollToTop = function () {
+       this.$element.scrollTop( 0 );
+};
+
 /**
  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
@@ -8214,12 +8102,12 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
        OO.ui.mixin.IconElement.call( this, config );
        OO.ui.mixin.IndicatorElement.call( this, config );
        OO.ui.mixin.LabelElement.call( this, config );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.TitledElement.call( this, $.extend( {
                $titled: this.$label
-       } ) );
-       OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.TabIndexedElement.call( this, $.extend( {
                $tabIndexed: this.$handle
-       } ) );
+       }, config ) );
 
        // Properties
        this.menu = new OO.ui.MenuSelectWidget( $.extend( {
@@ -9106,7 +8994,6 @@ OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
  * @abstract
  * @class
  * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.FlaggedElement
  * @mixins OO.ui.mixin.TabIndexedElement
  * @mixins OO.ui.mixin.TitledElement
  * @mixins OO.ui.mixin.AccessKeyedElement
@@ -9134,16 +9021,15 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) {
        this.inputFilter = config.inputFilter;
 
        // Mixin constructors
-       OO.ui.mixin.FlaggedElement.call( this, config );
-       OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.TabIndexedElement.call( this, $.extend( {
                $tabIndexed: this.$input
-       } ) );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.TitledElement.call( this, $.extend( {
                $titled: this.$input
-       } ) );
-       OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, {
+       }, config ) );
+       OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {
                $accessKeyed: this.$input
-       } ) );
+       }, config ) );
 
        // Events
        this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
@@ -9168,7 +9054,6 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
@@ -9415,6 +9300,7 @@ OO.ui.HiddenInputWidget.static.tagName = 'input';
  * @mixins OO.ui.mixin.IconElement
  * @mixins OO.ui.mixin.IndicatorElement
  * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.FlaggedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -9442,12 +9328,13 @@ OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
        OO.ui.ButtonInputWidget.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.ButtonElement.call( this, $.extend( {
                $button: this.$input
-       } ) );
+       }, config ) );
        OO.ui.mixin.IconElement.call( this, config );
        OO.ui.mixin.IndicatorElement.call( this, config );
        OO.ui.mixin.LabelElement.call( this, config );
+       OO.ui.mixin.FlaggedElement.call( this, config );
 
        // Initialization
        if ( !config.useInputTag ) {
@@ -9463,6 +9350,7 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.FlaggedElement );
 
 /* Static Properties */
 
@@ -9579,6 +9467,7 @@ OO.ui.ButtonInputWidget.prototype.getInputId = function () {
  * @param {Object} [config] Configuration options
  * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
  *  not selected.
+ * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
  */
 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
        // Configuration initialization
@@ -9599,12 +9488,24 @@ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
                // Required for pretty styling in WikimediaUI theme
                .append( this.checkIcon.$element );
        this.setSelected( config.selected !== undefined ? config.selected : false );
+       this.setIndeterminate( config.indeterminate !== undefined ? config.indeterminate : false );
 };
 
 /* Setup */
 
 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
 
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the state of the input changes.
+ *
+ * @param {boolean} selected
+ * @param {boolean} indeterminate
+ */
+
 /* Static Properties */
 
 /**
@@ -9643,6 +9544,7 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
                // Allow the stack to clear so the value will be updated
                setTimeout( function () {
                        widget.setSelected( widget.$input.prop( 'checked' ) );
+                       widget.setIndeterminate( widget.$input.prop( 'indeterminate' ) );
                } );
        }
 };
@@ -9650,16 +9552,20 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
 /**
  * Set selection state of this checkbox.
  *
- * @param {boolean} state `true` for selected
+ * @param {boolean} state Selected state
+ * @param {boolean} internal Used for internal calls to suppress events
  * @chainable
- * @return {OO.ui.Widget} The widget, for chaining
+ * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
  */
-OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
+OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state, internal ) {
        state = !!state;
        if ( this.selected !== state ) {
                this.selected = state;
                this.$input.prop( 'checked', this.selected );
-               this.emit( 'change', this.selected );
+               if ( !internal ) {
+                       this.setIndeterminate( false, true );
+                       this.emit( 'change', this.selected, this.indeterminate );
+               }
        }
        // The first time that the selection state is set (probably while constructing the widget),
        // remember it in defaultSelected. This property can be later used to check whether
@@ -9686,6 +9592,42 @@ OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
        return this.selected;
 };
 
+/**
+ * Set indeterminate state of this checkbox.
+ *
+ * @param {boolean} state Indeterminate state
+ * @param {boolean} internal Used for internal calls to suppress events
+ * @chainable
+ * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
+ */
+OO.ui.CheckboxInputWidget.prototype.setIndeterminate = function ( state, internal ) {
+       state = !!state;
+       if ( this.indeterminate !== state ) {
+               this.indeterminate = state;
+               this.$input.prop( 'indeterminate', this.indeterminate );
+               if ( !internal ) {
+                       this.setSelected( false, true );
+                       this.emit( 'change', this.selected, this.indeterminate );
+               }
+       }
+       return this;
+};
+
+/**
+ * Check if this checkbox is selected.
+ *
+ * @return {boolean} Checkbox is selected
+ */
+OO.ui.CheckboxInputWidget.prototype.isIndeterminate = function () {
+       // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+       // it, and we won't know unless they're kind enough to trigger a 'change' event.
+       var indeterminate = this.$input.prop( 'indeterminate' );
+       if ( this.indeterminate !== indeterminate ) {
+               this.setIndeterminate( indeterminate );
+       }
+       return this.indeterminate;
+};
+
 /**
  * @inheritdoc
  */
@@ -10575,7 +10517,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () {
  *     // A TextInputWidget.
  *     var textInput = new OO.ui.TextInputWidget( {
  *         value: 'Text input'
- *     } )
+ *     } );
  *     $( document.body ).append( textInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
@@ -10586,6 +10528,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () {
  * @mixins OO.ui.mixin.IndicatorElement
  * @mixins OO.ui.mixin.PendingElement
  * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.FlaggedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -10627,8 +10570,9 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Mixin constructors
        OO.ui.mixin.IconElement.call( this, config );
        OO.ui.mixin.IndicatorElement.call( this, config );
-       OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
+       OO.ui.mixin.PendingElement.call( this, $.extend( { $pending: this.$input }, config ) );
        OO.ui.mixin.LabelElement.call( this, config );
+       OO.ui.mixin.FlaggedElement.call( this, config );
 
        // Properties
        this.type = this.getSaneType( config );
@@ -10698,6 +10642,7 @@ OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.FlaggedElement );
 
 /* Static Properties */
 
@@ -11292,6 +11237,7 @@ OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) {
        this.connect( this, {
                change: 'onChange'
        } );
+       this.$indicator.on( 'click', this.onIndicatorClick.bind( this ) );
 
        // Initialization
        this.updateSearchIndicator();
@@ -11315,9 +11261,12 @@ OO.ui.SearchInputWidget.prototype.getSaneType = function () {
 };
 
 /**
- * @inheritdoc
+ * Handle click events on the indicator
+ *
+ * @param {jQuery.Event} e Click event
+ * @return {boolean}
  */
-OO.ui.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
+OO.ui.SearchInputWidget.prototype.onIndicatorClick = function ( e ) {
        if ( e.which === OO.ui.MouseButtons.LEFT ) {
                // Clear the text field
                this.setValue( '' );
@@ -11380,8 +11329,8 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
  *     // A MultilineTextInputWidget.
  *     var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
  *         value: 'Text input on multiple lines'
- *     } )
- *     $( 'body' ).append( multilineTextInput.$element );
+ *     } );
+ *     $( document.body ).append( multilineTextInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
  *
@@ -11940,6 +11889,8 @@ OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
  *  displayed below the widget.
  * @cfg {Array} [warnings] Warning messages about the widget, which will be
  *  displayed below the widget.
+ * @cfg {Array} [successMessages] Success messages on user interactions with the widget,
+ *  which will be displayed below the widget.
  *  The array may contain strings or OO.ui.HtmlSnippet instances.
  * @cfg {Array} [notices] Notices about the widget, which will be displayed
  *  below the widget.
@@ -11978,15 +11929,16 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        OO.ui.FieldLayout.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+       OO.ui.mixin.LabelElement.call( this, $.extend( {
                $label: $( '<label>' )
-       } ) );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
+       }, config ) );
+       OO.ui.mixin.TitledElement.call( this, $.extend( { $titled: this.$label }, config ) );
 
        // Properties
        this.fieldWidget = fieldWidget;
        this.errors = [];
        this.warnings = [];
+       this.successMessages = [];
        this.notices = [];
        this.$field = this.isFieldInline() ? $( '<span>' ) : $( '<div>' );
        this.$messages = $( '<ul>' );
@@ -12032,6 +11984,7 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
 
        this.setErrors( config.errors || [] );
        this.setWarnings( config.warnings || [] );
+       this.setSuccess( config.successMessages || [] );
        this.setNotices( config.notices || [] );
        this.setAlignment( config.align );
        // Call this again to take into account the widget's accessKey
@@ -12091,6 +12044,8 @@ OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
        } else if ( kind === 'warning' ) {
                $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
                $listItem.attr( 'role', 'alert' );
+       } else if ( kind === 'success' ) {
+               $icon = new OO.ui.IconWidget( { icon: 'check', flags: [ 'success' ] } ).$element;
        } else if ( kind === 'notice' ) {
                $icon = new OO.ui.IconWidget( { icon: 'notice' } ).$element;
        } else {
@@ -12190,6 +12145,21 @@ OO.ui.FieldLayout.prototype.setWarnings = function ( warnings ) {
        return this;
 };
 
+/**
+ * Set the list of success messages.
+ *
+ * @param {Array} successMessages Success messages about the widget, which will be displayed below
+ *  the widget.
+ *  The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @chainable
+ * @return {OO.ui.BookletLayout} The layout, for chaining
+ */
+OO.ui.FieldLayout.prototype.setSuccess = function ( successMessages ) {
+       this.successMessages = successMessages.slice();
+       this.updateMessages();
+       return this;
+};
+
 /**
  * Set the list of notice messages.
  *
@@ -12205,7 +12175,7 @@ OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
 };
 
 /**
- * Update the rendering of error, warning and notice messages.
+ * Update the rendering of error, warning, success and notice messages.
  *
  * @private
  */
@@ -12213,7 +12183,12 @@ OO.ui.FieldLayout.prototype.updateMessages = function () {
        var i;
        this.$messages.empty();
 
-       if ( this.errors.length || this.warnings.length || this.notices.length ) {
+       if (
+               this.errors.length ||
+               this.warnings.length ||
+               this.successMessages.length ||
+               this.notices.length
+       ) {
                this.$body.after( this.$messages );
        } else {
                this.$messages.remove();
@@ -12226,6 +12201,9 @@ OO.ui.FieldLayout.prototype.updateMessages = function () {
        for ( i = 0; i < this.warnings.length; i++ ) {
                this.$messages.append( this.makeMessage( 'warning', this.warnings[ i ] ) );
        }
+       for ( i = 0; i < this.successMessages.length; i++ ) {
+               this.$messages.append( this.makeMessage( 'success', this.successMessages[ i ] ) );
+       }
        for ( i = 0; i < this.notices.length; i++ ) {
                this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
        }
@@ -12506,21 +12484,21 @@ OO.ui.FieldsetLayout.static.tagName = 'fieldset';
  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
  *     @example
- *     // Example of a form layout that wraps a fieldset layout
+ *     // Example of a form layout that wraps a fieldset layout.
  *     var input1 = new OO.ui.TextInputWidget( {
- *         placeholder: 'Username'
- *     } );
- *     var input2 = new OO.ui.TextInputWidget( {
- *         placeholder: 'Password',
- *         type: 'password'
- *     } );
- *     var submit = new OO.ui.ButtonInputWidget( {
- *         label: 'Submit'
- *     } );
+ *             placeholder: 'Username'
+ *         } ),
+ *         input2 = new OO.ui.TextInputWidget( {
+ *             placeholder: 'Password',
+ *             type: 'password'
+ *         } ),
+ *         submit = new OO.ui.ButtonInputWidget( {
+ *             label: 'Submit'
+ *         } ),
+ *         fieldset = new OO.ui.FieldsetLayout( {
+ *             label: 'A form layout'
+ *         } );
  *
- *     var fieldset = new OO.ui.FieldsetLayout( {
- *         label: 'A form layout'
- *     } );
  *     fieldset.addItems( [
  *         new OO.ui.FieldLayout( input1, {
  *             label: 'Username',
@@ -12560,7 +12538,7 @@ OO.ui.FormLayout = function OoUiFormLayout( config ) {
        OO.ui.FormLayout.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+       OO.ui.mixin.GroupElement.call( this, $.extend( { $group: this.$element }, config ) );
 
        // Events
        this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
@@ -12679,6 +12657,19 @@ OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
 
 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
 
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.PanelLayout.static.reusePreInfuseDOM = function ( node, config ) {
+       config = OO.ui.PanelLayout.parent.static.reusePreInfuseDOM( node, config );
+       if ( config.preserveContent !== false ) {
+               config.$content = $( node ).contents();
+       }
+       return config;
+};
+
 /* Methods */
 
 /**
@@ -12698,7 +12689,7 @@ OO.ui.PanelLayout.prototype.focus = function () {
  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
  *
  *     @example
- *     // HorizontalLayout with a text input and a label
+ *     // HorizontalLayout with a text input and a label.
  *     var layout = new OO.ui.HorizontalLayout( {
  *       items: [
  *         new OO.ui.LabelWidget( { label: 'Label' } ),
@@ -12723,7 +12714,7 @@ OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
        OO.ui.HorizontalLayout.parent.call( this, config );
 
        // Mixin constructors
-       OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+       OO.ui.mixin.GroupElement.call( this, $.extend( { $group: this.$element }, config ) );
 
        // Initialization
        this.$element.addClass( 'oo-ui-horizontalLayout' );
@@ -13111,6 +13102,263 @@ OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
        return this;
 };
 
+/**
+ * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
+ * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
+ * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
+ * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
+ *
+ *     @example
+ *     // A file select input widget.
+ *     var selectFile = new OO.ui.SelectFileInputWidget();
+ *     $( document.body ).append( selectFile.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
+ * @cfg {string} [placeholder] Text to display when no file is selected.
+ * @cfg {Object} [button] Config to pass to select file button.
+ * @cfg {string} [icon] Icon to show next to file info
+ */
+OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
+       config = config || {};
+
+       // Construct buttons before parent method is called (calling setDisabled)
+       this.selectButton = new OO.ui.ButtonWidget( $.extend( {
+               $element: $( '<label>' ),
+               classes: [ 'oo-ui-selectFileInputWidget-selectButton' ],
+               label: OO.ui.msg( 'ooui-selectfile-button-select' )
+       }, config.button ) );
+
+       // Configuration initialization
+       config = $.extend( {
+               accept: null,
+               placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
+               $tabIndexed: this.selectButton.$tabIndexed
+       }, config );
+
+       this.info = new OO.ui.SearchInputWidget( {
+               classes: [ 'oo-ui-selectFileInputWidget-info' ],
+               placeholder: config.placeholder,
+               // Pass an empty collection so that .focus() always does nothing
+               $tabIndexed: $( [] )
+       } ).setIcon( config.icon );
+       // Set tabindex manually on $input as $tabIndexed has been overridden
+       this.info.$input.attr( 'tabindex', -1 );
+
+       // Parent constructor
+       OO.ui.SelectFileInputWidget.parent.call( this, config );
+
+       // Properties
+       this.currentFile = null;
+       if ( Array.isArray( config.accept ) ) {
+               this.accept = config.accept;
+       } else {
+               this.accept = null;
+       }
+       this.onFileSelectedHandler = this.onFileSelected.bind( this );
+
+       // Events
+       this.info.connect( this, { change: 'onInfoChange' } );
+       this.selectButton.$button.on( {
+               keypress: this.onKeyPress.bind( this )
+       } );
+       this.connect( this, { change: 'updateUI' } );
+
+       // Initialization
+       this.setupInput();
+
+       this.fieldLayout = new OO.ui.ActionFieldLayout( this.info, this.selectButton, { align: 'top' } );
+
+       this.$element
+               .addClass( 'oo-ui-selectFileInputWidget' )
+               .append( this.fieldLayout.$element );
+
+       this.updateUI();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SelectFileInputWidget, OO.ui.InputWidget );
+
+/* Methods */
+
+/**
+ * Get the filename of the currently selected file.
+ *
+ * @return {string} Filename
+ */
+OO.ui.SelectFileInputWidget.prototype.getFilename = function () {
+       if ( this.currentFile ) {
+               return this.currentFile.name;
+       } else {
+               // Try to strip leading fakepath.
+               return this.getValue().split( '\\' ).pop();
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SelectFileInputWidget.prototype.setValue = function ( value ) {
+       if ( value === undefined ) {
+               // Called during init, don't replace value if just infusing.
+               return;
+       }
+       if ( value ) {
+               // We need to update this.value, but without trying to modify
+               // the DOM value, which would throw an exception.
+               if ( this.value !== value ) {
+                       this.value = value;
+                       this.emit( 'change', this.value );
+               }
+       } else {
+               this.currentFile = null;
+               // Parent method
+               OO.ui.SelectFileInputWidget.super.prototype.setValue.call( this, '' );
+       }
+};
+
+/**
+ * Handle file selection from the input.
+ *
+ * @protected
+ * @param {jQuery.Event} e
+ */
+OO.ui.SelectFileInputWidget.prototype.onFileSelected = function ( e ) {
+       var file = OO.getProp( e.target, 'files', 0 ) || null;
+
+       if ( file && !this.isAllowedType( file.type ) ) {
+               file = null;
+       }
+
+       this.currentFile = file;
+};
+
+/**
+ * Update the user interface when a file is selected or unselected.
+ *
+ * @protected
+ */
+OO.ui.SelectFileInputWidget.prototype.updateUI = function () {
+       this.info.setValue( this.getFilename() );
+};
+
+/**
+ * Setup the input element.
+ *
+ * @protected
+ */
+OO.ui.SelectFileInputWidget.prototype.setupInput = function () {
+       var widget = this;
+       this.$input
+               .attr( {
+                       type: 'file',
+                       // this.selectButton is tabindexed
+                       tabindex: -1,
+                       // Infused input may have previously by
+                       // TabIndexed, so remove aria-disabled attr.
+                       'aria-disabled': null
+               } )
+               .on( 'change', this.onFileSelectedHandler )
+               // Support: IE11
+               // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
+               // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
+               // Since this messes with our custom styling (the file input has large dimensions and this
+               // causes the label to scroll out of view), scroll the button back to top. (T192131)
+               .on( 'focus', function () {
+                       widget.$input.parent().prop( 'scrollTop', 0 );
+               } );
+
+       if ( this.accept ) {
+               this.$input.attr( 'accept', this.accept.join( ', ' ) );
+       }
+       this.selectButton.$button.append( this.$input );
+};
+
+/**
+ * Determine if we should accept this file.
+ *
+ * @private
+ * @param {string} mimeType File MIME type
+ * @return {boolean}
+ */
+OO.ui.SelectFileInputWidget.prototype.isAllowedType = function ( mimeType ) {
+       var i, mimeTest;
+
+       if ( !this.accept || !mimeType ) {
+               return true;
+       }
+
+       for ( i = 0; i < this.accept.length; i++ ) {
+               mimeTest = this.accept[ i ];
+               if ( mimeTest === mimeType ) {
+                       return true;
+               } else if ( mimeTest.substr( -2 ) === '/*' ) {
+                       mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
+                       if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
+                               return true;
+                       }
+               }
+       }
+
+       return false;
+};
+
+/**
+ * Handle info input change events
+ *
+ * The info widget can only be changed by the user
+ * with the clear button.
+ *
+ * @private
+ * @param {string} value
+ */
+OO.ui.SelectFileInputWidget.prototype.onInfoChange = function ( value ) {
+       if ( value === '' ) {
+               this.setValue( null );
+       }
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ * @return {undefined/boolean} False to prevent default if event is handled
+ */
+OO.ui.SelectFileInputWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && this.$input &&
+               ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+       ) {
+               // Emit a click to open the file selector.
+               this.$input.trigger( 'click' );
+               // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
+               this.selectButton.onDocumentKeyUp( e );
+               return false;
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SelectFileInputWidget.prototype.setDisabled = function ( disabled ) {
+       // Parent method
+       OO.ui.SelectFileInputWidget.parent.prototype.setDisabled.call( this, disabled );
+
+       this.selectButton.setDisabled( disabled );
+       this.info.setDisabled( disabled );
+
+       return this;
+};
+
 }( OO ) );
 
 //# sourceMappingURL=oojs-ui-core.js.map.json
\ No newline at end of file